在上一章中,我們成功讓容器之間的封包可以正常傳遞,但容器對外的封包傳遞問題仍未解決。本章我們將逐步探討這個問題,首先來檢查 ns1 到 root namespace 之間可能缺少的部分。
讓我們重新回憶上次的操作和訊息:
# 從 ns1 ping Host IP
$ sudo ip netns exec ns1 ping -c 2 172.31.39.53
###
ping: connect: Network is unreachable
######
# 從 Host ping 位於 ns0 的 veth0 IP
$ ping -c 2 172.18.0.2
###
PING 172.18.0.2 (172.18.0.2) 56(84) bytes of data.
^C
--- 172.18.0.2 ping statistics ---
2 packets transmitted, 0 received, 100% packet loss, time 1004ms
ping: connect: Network is unreachable
這個錯誤表示你的系統無法找到通往目標網路的路徑。這意味著:
簡單來說,這訊息就是告訴你:「我想去那個地方,但是找不到路。」。
路由表(Routing Table)是一張指導網路流量(數據包)如何從源地址送到目標地址的地圖。它告訴系統每一個目標網段應該經由哪條路徑、哪個網關(下一跳)或哪個網路接口來到達目的地。
我認為可以用早期的電話中心(Call Center)來理解路由表。每次打電話需要人工接線員將你的線路插到對方的端口,讓兩人通話。路由表就像這個接線員的「電話分配表」,決定每條線路(數據包)應該接到哪個插孔(下一跳)。
依照前面提到的執行規則,路由表的類型通常可分為以下兩類:
每一條路由條目通常會包括:
192.168.1.0/24
(內網)或 0.0.0.0/0
(所有未知目標)。0.0.0.0
)通常經由網關 192.168.1.1
。U
:表示這條路由是「啟用」的。G
:表示這條路由是指向 gateway(即需要轉發的路由)。在 Linux 中,可以使用 route
或 ip route
命令來查看路由表。這些命令會顯示系統如何轉發網路封包,尤其是選擇哪些路由來到達不同的目標網路。
我覺得 route
指令比較好理解,不過 Ubuntu 並沒有預先安裝 route
,可以使用以下指令安裝:
apt-get update
apt-get install net-tools
查詢 routing table:
route -n
-n
代表顯示 IP 數字,而不要顯示 hostnames。
查詢結果類似如下:
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 192.168.1.1 0.0.0.0 UG 100 0 0 eth0
192.168.1.0 0.0.0.0 255.255.255.0 U 0 0 0 eth0
10.0.0.0 192.168.1.254 255.255.255.0 UG 200 0 0 eth0
172.16.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0
192.168.1.23
(同一個社區),路由表會說:「不需要經過網關,直接送到社區內的地址,使用 eth0
」。10.0.0.45
(另一個城市),路由表會說:「找到中轉站 192.168.1.254
,經過它送信,使用 eth0
」。8.8.8.8
),路由表會說:「不知道怎麼處理,發給默認網關 192.168.1.1
來處理,使用 eth0
」。我們先來觀察 ns1 路由表的內容:
$ sudo ip netns exec ns1 route -n
###
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
172.18.0.0 0.0.0.0 255.255.255.0 U 0 0 0 veth1
依照上面我們看到的邏輯,當我們 ping 另一個容器 (172.18.0.2
) 時,封包會依照這個條目,透過 veth1 送出去,經由 veth pair 到另一端網路介面 veth1-br,再透過 bridge 傳送到 veth0-br,然後又透過 veth pair 送到另一端的 veth0。這也是為什麼容器與容器間的傳送是暢通的原因。
我們可以使用 tracepath 來追蹤網路封包的路徑(預設使用 UDP 封包),它簡單並且不需要額外的權限,適合快速排查。不過需要注意的是,tracepath 無法追蹤 ICMP 封包,這限制了它對某些情況的適用性。
$ sudo ip netns exec ns1 tracepath -n 172.18.0.2
1?: [LOCALHOST] pmtu 1500
1: 172.18.0.2 0.063ms reached
1: 172.18.0.2 0.042ms reached
Resume: pmtu 1500 hops 1 back 2
但很顯然的,當我們的目標是 Host IP (172.31.39.53) 時,這張表就無法指引我們該往哪裡去,因此就失敗了。
sudo ip netns exec ns1 tracepath -n 172.31.39.53
1: send failed
Resume: pmtu 65535
$ route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 172.31.32.1 0.0.0.0 UG 512 0 0 enX0
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0
172.31.0.2 172.31.32.1 255.255.255.255 UGH 512 0 0 enX0
172.31.32.0 0.0.0.0 255.255.240.0 U 512 0 0 enX0
172.31.32.1 0.0.0.0 255.255.255.255 UH 512 0 0 enX0
我們來分析開頭提到的案例,"從 Host ping 位於 ns0 的 veth0 IP (172.18.0.2)",會發生什麼事。
172.18.0.2
不被其他 Destination 條目選中,所以它跳到了條目 1,走網關 172.31.32.1
。$ tracepath -n -m 10 172.18.0.2
1?: [LOCALHOST] pmtu 9001
1: no reply
2: no reply
3: no reply
4: no reply
5: no reply
6: no reply
7: no reply
8: no reply
9: no reply
10: no reply
Too many hops: pmtu 9001
Resume: pmtu 9001
接下來我們要透過調整路由表,來解決剛剛論證的兩個問題:
第一個問題出乎意料的好解決,只需要為 docker1 這個網路介面加上可以覆蓋 vth0, vth1 的 IP 網段:
sudo ip addr add 172.18.0.1/24 dev docker1
查詢 docker1 的 iptable:
$ ip -br addr
###
lo UNKNOWN 127.0.0.1/8 ::1/128
enX0 UP 172.31.39.53/20 metric 512 fe80::49d:2ff:fe88:529d/64
docker0 DOWN 172.17.0.1/16
docker1 UP 172.18.0.1/24 fe80::f8b6:c0ff:fe55:3371/64
veth0-br@if6 UP fe80::7c56:1aff:fe0a:5c8b/64
veth1-br@if8 UP fe80::3065:8eff:fe22:180/64
還記得上面提到的 Directly connected route 嗎?
當某個網路介面(interface)有一個 IP 地址時,該網路介面就可以直接訪問與該 IP 屬於同一網段的其他設備。
自動添加的直連路由條目只會在以下情況發生:
- IP 地址包含有子網資訊(例如 /24 的子網掩碼)。
- 介面是啟動狀態(
ip link set <interface> up
)。
Linux 的內核網路堆疊實現了這個自動行為:當你使用 ip addr add
添加 IP 時,內核會同步更新路由表:
$ route -n
###
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 172.31.32.1 0.0.0.0 UG 512 0 0 enX0
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0
172.18.0.0 0.0.0.0 255.255.255.0 U 0 0 0 docker1 <- 更新的條目
172.31.0.2 172.31.32.1 255.255.255.255 UGH 512 0 0 enX0
172.31.32.0 0.0.0.0 255.255.240.0 U 512 0 0 enX0
172.31.32.1 0.0.0.0 255.255.255.255 UH 512 0 0 enX0
現在我們就可以從 Host 與 veth0 建立連線了:
$ ping -c 2 172.18.0.2
PING 172.18.0.2 (172.18.0.2) 56(84) bytes of data.
64 bytes from 172.18.0.2: icmp_seq=1 ttl=127 time=0.057 ms
64 bytes from 172.18.0.2: icmp_seq=2 ttl=127 time=0.041 ms
--- 172.18.0.2 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1047ms
rtt min/avg/max/mdev = 0.041/0.049/0.057/0.008 ms
$ tracepath -n 172.18.0.2
1?: [LOCALHOST] pmtu 1500
1: 172.18.0.2 0.045ms reached
1: 172.18.0.2 0.009ms reached
Resume: pmtu 1500 hops 1 back 2
如果我們仔細對比 root namespace 和 ns0, ns1 的路由表,就會發現: ns0, ns1 似乎沒有預設路由。
回想一下,我們在 root namespace 中,是有看到預設條目的:
0.0.0.0 172.31.32.1 0.0.0.0 UG 512 0 0 enX0
我們在本機的 WSL 環境,開啟一個新的 container 來比較一下:
$ docker container run --rm -it alpine /bin/sh
/ # route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 172.17.0.1 0.0.0.0 UG 0 0 0 eth0
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 eth0
/ # ip route
default via 172.17.0.1 dev eth0
172.17.0.0/16 dev eth0 scope link src 172.17.0.2
/ # ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
11: eth0@if12: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
因此第二個問題的解決方法,就是要為 ns0, ns1 補上缺失的預設條目。指向 docker1 這個網路介面 IP:
# 添加預設條目 - ns0
sudo ip netns exec ns0 ip route add default via 172.18.0.1
# 添加預設條目 - ns1
sudo ip netns exec ns1 ip route add default via 172.18.0.1
查詢路由表:
# ns0
$ sudo ip netns exec ns0 ip route
###
default via 172.18.0.1 dev veth0
172.18.0.0/24 dev veth0 proto kernel scope link src 172.18.0.2
######
# ns1
$ sudo ip netns exec ns1 ip route
###
default via 172.18.0.1 dev veth1
172.18.0.0/24 dev veth1 proto kernel scope link src 172.18.0.3
測試結果:
# ns0
$ sudo ip netns exec ns0 ping -c 2 172.31.39.53
###
PING 172.31.39.53 (172.31.39.53) 56(84) bytes of data.
64 bytes from 172.31.39.53: icmp_seq=1 ttl=127 time=0.076 ms
64 bytes from 172.31.39.53: icmp_seq=2 ttl=127 time=0.046 ms
--- 172.31.39.53 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1001ms
rtt min/avg/max/mdev = 0.046/0.061/0.076/0.015 ms
######
# ns1
$ sudo ip netns exec ns1 ping -c 2 172.31.39.53
###
PING 172.31.39.53 (172.31.39.53) 56(84) bytes of data.
64 bytes from 172.31.39.53: icmp_seq=1 ttl=127 time=0.052 ms
64 bytes from 172.31.39.53: icmp_seq=2 ttl=127 time=0.045 ms
--- 172.31.39.53 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1065ms
rtt min/avg/max/mdev = 0.045/0.048/0.052/0.003 ms
這樣一來,我們已經解決了 bridge 之間的封包傳遞問題,至於容器與外部網路的溝通部分,留到下一章再說。我們下次見。